package com.ljc.gateway;


import com.ljc.constant.RedisConstant;
import com.ljc.model.entity.InterfaceInfo;
import com.ljc.model.entity.User;
import com.ljc.service.InnerInterfaceInfoService;
import com.ljc.service.InnerUserInterfaceInfoService;
import com.ljc.service.InnerUserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.dubbo.config.annotation.DubboReference;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

import static com.ljc.utils.SignUtils.genSign;


/**
 * 接口请求过滤器
 */
@Component
@Slf4j
public class InterfaceFilter implements GatewayFilter, Ordered {

    private static final List<String> IP_WHITE_LIST = Arrays.asList("127.0.0.1", "10.0.16.16");
    private static final String HOST = "http://localhost:8001";
    @DubboReference
    private InnerUserService innerUserService;
    @DubboReference
    private InnerUserInterfaceInfoService innerUserInterfaceInfoService;
    @DubboReference
    private InnerInterfaceInfoService innerInterfaceInfoService;
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1.请求日志
        ServerHttpRequest request = exchange.getRequest();
        String method = request.getMethod().toString();
        String path = HOST + request.getPath().value();
        log.info("请求唯一ID：" + request.getId());
        log.info("请求方法：" + method);
        log.info("请求路径：" + path);
        log.info("请求参数：" + request.getQueryParams());
        String sourceAddress = request.getLocalAddress().getHostString();
        log.info("请求来源地址：" + sourceAddress);

        //拿到响应体，用作返回
        ServerHttpResponse response = exchange.getResponse();

        // 2.黑白名单
        if (!IP_WHITE_LIST.contains(sourceAddress)) {
            return handleNoAuth(response);
        }

        HttpHeaders headers = request.getHeaders();
        // 3.用户鉴权，检验ak sk
        String accessKey = headers.getFirst("accessKey");
        if (StringUtils.isEmpty(accessKey)) {
            return handleNoAuth(response);
        }
        String nonce = headers.getFirst("nonce");
        String body;
        // 使用UTF-8解码请求体
        try {
            body = URLDecoder.decode(headers.getFirst("body"), "utf-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
        String timestamp = headers.getFirst("timestamp");
        String sign = headers.getFirst("sign");

        // 从数据库中查出该accessKey对应的用户
        String userIdStr = null;
        String secretKey = null;
        try {
            // TODO 处理缓存击穿
            // 使用Redis缓存用户和接口信息
            userIdStr = (String) stringRedisTemplate.opsForHash().get(RedisConstant.USER_AK + accessKey, "id");
            secretKey = (String) stringRedisTemplate.opsForHash().get(RedisConstant.USER_AK + accessKey, "secretKey");
            if (StringUtils.isEmpty(userIdStr) || StringUtils.isEmpty(secretKey)) {
                // 缓存中没有数据，从数据库中查询后缓存到Redis中
                User invokeUser = innerUserService.getInvokeUser(accessKey);
                if (invokeUser == null) {
                    return handleNoAuth(response);
                }
                userIdStr = String.valueOf(invokeUser.getId());
                secretKey = invokeUser.getSecretKey();
                HashMap<String, String> redisMap = new HashMap<>();
                redisMap.put("id", userIdStr);
                redisMap.put("secretKey", secretKey);
                stringRedisTemplate.opsForHash().putAll(RedisConstant.USER_AK + accessKey, redisMap);
            }
        } catch (Exception e) {
            log.error("getInvokeUser error:{}", e.getMessage());
        }
        Long userId = Long.valueOf(userIdStr);

        if (nonce.length() > 5) {
            return handleNoAuth(response);
        }
        //  应该比较当前时间距离请求头中的时间，判断是否过期
        long currentTimeMillis = System.currentTimeMillis() / 1000;
        final Long FIVE_MINUTES = 60 * 5L;
        if (currentTimeMillis - Long.parseLong(timestamp) >= FIVE_MINUTES) {
            return handleNoAuth(response);
        }

        //  这里的secretKey应该从上面查出的用户中获取
        if (sign == null || !sign.equals(genSign(body, secretKey))) {
            return handleNoAuth(response);
        }

        // 4.判断请求接口是否存在
        //  从数据库中查询，判断接口url和方法类型，通过远程调用
        Long interfaceInfoId = null;
        try {
            // TODO 处理缓存击穿
            // 使用Redis优化，从缓存中查询接口信息
            String interfaceInfoIdStr = (String) stringRedisTemplate.opsForHash().get(RedisConstant.INTERFACE_INVOKE_METHOD_PREFIX + method, path);
            if (StringUtils.isNotEmpty(interfaceInfoIdStr)) {
                interfaceInfoId = Long.parseLong(interfaceInfoIdStr);
            } else {
                InterfaceInfo interfaceInfo = innerInterfaceInfoService.getInterfaceInfo(path, method);
                if (interfaceInfo == null) {
                    log.error("接口不存在 路径，方法:{}，{}", path, method);
                    return handleNoAuth(response);
                }
                interfaceInfoId = interfaceInfo.getId();
                // 缓存重建
                HashMap<String, String> map = new HashMap<>();
                map.put(interfaceInfo.getUrl(), String.valueOf(interfaceInfoId));
                stringRedisTemplate.opsForHash().putAll(RedisConstant.INTERFACE_INVOKE_METHOD_PREFIX + interfaceInfo.getMethod(), map);
            }
        } catch (Exception e) {
            log.error("getInterfaceInfo error:{}", e.getMessage());
        }
        if (interfaceInfoId == null) {
            log.error("接口不存在 路径，方法:{}，{}", path, method);
            return handleNoAuth(response);
        }

        // 6.响应日志
        log.info("响应：" + response.getStatusCode());

        // TODO 使用Redis优化 7. 判断是否还有调用次数
        boolean hasAuth = innerUserInterfaceInfoService.checkUserInvokeAuth(userId, interfaceInfoId);
        if (!hasAuth) {
            log.error("接口没有调用次数 用户id，接口id:{}，{}", userId, interfaceInfoId);
            return handleNoAuth(response);
        }

        //请求转发，响应日志
        return handleResponse(exchange, chain, interfaceInfoId, userId);
    }

    public Mono<Void> handleResponse(ServerWebExchange exchange, GatewayFilterChain chain, Long interfaceInfoId, Long userId) {
        try {
            ServerHttpResponse originalResponse = exchange.getResponse();
            DataBufferFactory bufferFactory = originalResponse.bufferFactory();

            HttpStatus statusCode = originalResponse.getStatusCode();

            if (statusCode == HttpStatus.OK) {
                ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
                    //等调用完接口后才会执行该方法
                    @Override
                    public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                        log.info("body instanceof Flux: {}", (body instanceof Flux));
                        if (body instanceof Flux) {
                            Flux<? extends DataBuffer> fluxBody = Flux.from(body);
                            return super.writeWith(fluxBody.map(dataBuffer -> {
                                //  7.调用成功，次数+1
                                try {
                                    // 在数据库层面进行调用次数的更新，保证数据的完整性和一致性
                                    innerUserInterfaceInfoService.invokeCount(interfaceInfoId, userId);
                                } catch (Exception e) {
                                    log.error("invokeCount error:", e);
                                }
                                byte[] content = new byte[dataBuffer.readableByteCount()];
                                dataBuffer.read(content);
                                DataBufferUtils.release(dataBuffer);//释放掉内存
                                // 构建日志
                                StringBuilder sb2 = new StringBuilder(200);
                                List<Object> rspArgs = new ArrayList<>();
                                rspArgs.add(originalResponse.getStatusCode());
                                String data = new String(content, StandardCharsets.UTF_8);//data 响应结果数据
                                sb2.append(data);
                                log.info("响应结果：{}", data);
                                return bufferFactory.wrap(content);
                            }));
                        } else {
                            log.error("<--- {} 响应code异常", getStatusCode());
                        }
                        return super.writeWith(body);
                    }
                };
                // 调用模拟接口
                return chain.filter(exchange.mutate().response(decoratedResponse).build());
            }
            //降级处理返回数据
            return chain.filter(exchange);
        } catch (Exception e) {
            log.error("网关响应异常：" + e);
            return chain.filter(exchange);
        }
    }


    public Mono<Void> handleNoAuth(ServerHttpResponse response) {
        response.setStatusCode(HttpStatus.FORBIDDEN);
        return response.setComplete();
    }

    public Mono<Void> handleInvokeError(ServerHttpResponse response) {
        response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
        return response.setComplete();
    }

    @Override
    public int getOrder() {
        return -2;
    }

}